Jetpack Compose
Integration of MobaiBiometric in Jetpack Compose based applications
This example shows how to integrate the Biometric Capture Session to your project. The Biometric Capture Session extracts a frames collection from the camera preview.
Create a compose activity.
ComposeBiometric
extends from MBCaptureSessionServiceDelegate
.
class ComposeBiometric : ComponentActivity(), MBCaptureSessionServiceDelegate {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val options = MBCaptureSessionOptions.Builder()
.automaticCapture(true)
.build()
val captureSessionService = MBCaptureSessionService(
context = this, this as LifecycleOwner,
options, callback = this
)
setContent {
//Starting screen here
}
}
override fun onCaptureFinished(result: MBCaptureSessionResult?) { }
override fun onCountdown(timeCounter: Int) { }
override fun onValidating(faceStatus: MBFaceStatus) { }
override fun onFailure(errorEnum: MBCaptureSessionError) { }
override fun onCaptureStarted() { }
}
Build an MBCaptureSessionOption.Builder
instance.. All arguments are optional. It contains the default options for performing the capture session. Those default options can be redefined. In this case the capture process is set to automatic.
val options = MBCaptureSessionOptions.Builder()
.automaticCapture(true)
.build()
MBCaptureSessionOption.Builder
arguments are optional.
Options description:
numberOfFrameToCollect
Describes the number of frames to collect during the capture sessionframeInterval
After collecting the first frame, is the number of frames to skip before collecting a framenumberOfFramesBeforeCapture
Describes the number of frames to skip before it starts collectingautomaticCapture
Tells whether the capture is automatic or manualtargetResolution
Are the resolution values from the caller devicetimeBeforeAutomaticCapture
Is the set time in seconds the capture should wait to start collecting framescameraSelector
Describes whether we want to use a front or rear camera
Creates an instance of MBCaptureSessionInstance
. This is the entry point configuration for the capture process.
val captureSessionService = MBCaptureSessionService(
context = this, this as LifecycleOwner,
options, callback = this
)
MBCaptureSessionService
arguments.
context
Is an instance of the caller's context needed to implement MBCameraService and MBCaptureSessionView.lifecycleOwner
Is an instance of the caller's life cycle owner needed to implement MBCameraService.options
Represents the capture session's entry point configuration for ist correct functioning.callback
Updates the caller with any change during the whole capture process, including a capture result, it has to be set to null while implementing MBCaptureSessionFragment.
Function onCaptureFinished
gets the result of the capture process if it is successfully finished.
override fun onCaptureFinished(result: MBCaptureSessionResult?) { }
MBCaptureSessionResult
arguments description:
faceImage
A high quality image captured after collecting frames (not implemented)captureSessionData
Is all the collected data in protobuf format (not implemented)frames
Are the frame collection.
Function onCountdown
is executed during the automatic capture. timeCounter
is the current time in seconds.
override fun onCountdown(timeCounter: Int) { }
Function onValidating
is executed when frames are being analyzed.
override fun onValidating(faceStatus: MBFaceStatus) { }
MBFaceStatus
entries:
TOO_FAR_AWAY
Face is too far awayTOO_CLOSE
Face is too closeTOO_FAR_UP
Face is too far upTOO_FAR_LEFT
Face is too far leftTOO_FAR_DOWN
Face is too far downTOO_FAR_RIGHT
Face is too far rightNOT_FOUND
Face not foundTOO_MANY
Too many facesVALID
Face is valid
Function onFailure
is executed when the capture process fails. MBCaptureSessionError
tells the type of error that occurred.
override fun onFailure(errorEnum: MBCaptureSessionError) { }
MBCaptureSessionError
entries:
UNABLE_TO_OPEN_CAMERA
Describes failure while opening the cameraUNABLE_TO_COLLECT_FRAMES
The capture has is aborted
Function onCaptureStarted
is executed every time a face is valid and starts capturing.
override fun onCaptureStarted() { }
Add the following dependencies in the build gradle app.
Enables LiveData for Jetpack Compose.
implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
def nav_version = "2.5.2"
Enables navigation in Jetpack Compose.
implementation "androidx.navigation:navigation-compose:$nav_version"
Coil
helps to display a bitmap.
//Coil
implementation("io.coil-kt:coil-compose:1.4.0")
Implement a capture screen
Create the layout that contains the capture session view.
//container.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/text_color_disabled"
android:id="@+id/ll_view_container">
</LinearLayout>
Implement the composable function that defines the capture screen/camera preview.
MBCaptureSessionService
instance of the entry point configuration for the capture process. MBFaceStatus
gets the face status every time it changes in the camera preview. MBCaptureSessionStatus
gets a capture status every time it changes.
//CaptureScreen.kt
@SuppressLint("UnsafeOptInUsageError")
@Composable
fun CaptureScreen(
captureSessionService: MBCaptureSessionService,
faceStatus: MBFaceStatus
) {
Surface(modifier = Modifier.fillMaxSize()) {
AndroidView(
factory = { context ->
View.inflate(context, R.layout.container, null).apply {
val llViewContainer = this.findViewById<LinearLayout>(R.id.ll_view_container)
if (captureSessionService.getCaptureSessionView().parent != null) {
(captureSessionService.getCaptureSessionView().parent as ViewGroup)
.removeView(captureSessionService.getCaptureSessionView())
}
llViewContainer.addView(captureSessionService.getCaptureSessionView())
captureSessionService.startCamera()
}
},
modifier = Modifier.fillMaxSize(),
)
if (!captureSessionService.getAutomaticCapture()) {
Box(modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.padding(bottom = 100.dp),
contentAlignment = Alignment.BottomCenter) {
Button(modifier = Modifier
.width(150.dp)
.height(50.dp),
onClick = {
captureSessionService.startCaptureSession()
}) {
Text(text = "Start Capture")
}
}
}
}
}
Inflates camera preview based UI into a Jetpack Compose UI using AndroidView
with that propose
AndroidView(
factory = { context ->
View.inflate(context, R.layout.container, null).apply {
val llViewContainer = this.findViewById<LinearLayout>(R.id.ll_view_container)
if (captureSessionService.getCaptureSessionView().parent != null) {
(captureSessionService.getCaptureSessionView().parent as ViewGroup)
.removeView(captureSessionService.getCaptureSessionView())
}
llViewContainer.addView(captureSessionService.getCaptureSessionView())
captureSessionService.startCamera()
}
},
modifier = Modifier.fillMaxSize(),
)
Inflates the layout container view.
View.inflate(context, R.layout.container, null)
Create a layout container instance to be able to add a child view to it.
val llViewContainer = this.findViewById<LinearLayout>(R.id.ll_view_container)
Add the capture session view to the container ViewContainer
.
if (captureSessionService.getCaptureSessionView().parent != null) {
(captureSessionService.getCaptureSessionView().parent as ViewGroup)
.removeView(captureSessionService.getCaptureSessionView())
}
llViewContainer.addView(captureSessionService.getCaptureSessionView())
Start the camera if permission is granted.
captureSessionService.startCamera()
Enable capture button if capture is manual or disable it if the capture is automatic.
if (!captureSessionService.getAutomaticCapture()) {
Box(modifier = Modifier
.height(200.dp)
.fillMaxWidth()
.padding(bottom = 100.dp),
contentAlignment = Alignment.BottomCenter) {
Button(modifier = Modifier
.width(150.dp)
.height(50.dp),
onClick = {
captureSessionService.startCaptureSession()
}) {
Text(text = "Start Capture")
}
}
}
Starts manual capture session if this is set to manual.
onClick = {
captureSessionService.startCaptureSession()
}
Implement a screen to display the collection of frames.
The composable ImagesScreen
function displays the collection of frames in a LazyVerticalGrid
. The function gets a list of nullable bitmaps as parameter.
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ImagesScreen(frames: List<Bitmap?>) {
if (frames.isNotEmpty()) {
Surface(modifier = Modifier
.fillMaxSize()
.padding(3.dp),
color = Color.DarkGray) {
if (frames.isNotEmpty() && !frames.contains(null)) {
LazyVerticalGrid(
cells = GridCells.Fixed(2),
content ={
items(frames) { frame ->
Box(modifier = Modifier
.width(200.dp)
.height(300.dp)
.padding(3.dp),
contentAlignment = Alignment.Center){
Image(painter = rememberImagePainter(data = frame),"Image")
}
}
}
)
}
}
}
}
ScreensRoute
lists the screens contained in the example app.
sealed class ScreensRoute(val route: String) {
object CaptureScreen: ScreensRoute("main_screen")
object ImagesScreen: ScreensRoute("image_screen")
}
CaptureSessionViewModel
updates and observes the different states of the MBFaceStatus
by using Livedata.
class CaptureSessionViewModel: ViewModel() {
private val _faceStatus: MutableLiveData<MBFaceStatus> = MutableLiveData(MBFaceStatus.NOT_FOUND)
val faceStatus: LiveData<MBFaceStatus> = _faceStatus
fun onFaceStatusChange(faceStatus: MBFaceStatus) {
_faceStatus.value = faceStatus
}
}
FrameCollectionViewModel
updates and observes the collection of frames we get within the capture session result by using Livedata.
class FrameCollectionViewModel : ViewModel(){
private val mutableFrameCollection = MutableLiveData<List<Bitmap?>>()
var frameCollection: LiveData<List<Bitmap?>> = mutableFrameCollection
fun setFrameCollection(frames: ArrayList<Bitmap?>) {
mutableFrameCollection.value = frames
}
}
The composable function CaptureNavigation
controls the navigation between screens. Gets as parameters an instance of MBCaptureSessionService
entry point configuration for the capture process, an instance of NavControllerCallback
that provide a navigation controller instance to ComposeBiometric
, an instance of CaptureSessionViewModel
that get every change in the capture session status, and finally an instance of FrameCollectionViewModel
@Composable
fun CaptureNavigation(
captureService: MBCaptureSessionService,
provider: NavControllerCallback,
captureSessionViewModel: CaptureSessionViewModel = viewModel(),
frameCollectionViewModel: FrameCollectionViewModel = viewModel()
){
val navController = rememberNavController()
provider.provideController(navController)
val faceStatus: MBFaceStatus by captureSessionViewModel.faceStatus.observeAsState(MBFaceStatus.NOT_FOUND)
val frameCollection: List<Bitmap?> by frameCollectionViewModel.frameCollection.observeAsState(listOf())
NavHost(navController = navController, startDestination = ScreensRoute.CaptureScreen.route) {
composable(ScreensRoute.CaptureScreen.route) {
CaptureScreen(captureSessionService = captureService, faceStatus = faceStatus)
}
composable(ScreensRoute.ImagesScreen.route) {
ImagesScreen(frames = frameCollection)
}
}
}
The NavController
is the central API for the Navigation component. It is stateful and keeps track of the back stack of composables that make up the screens in your app and the state of each screen.
Create a NavController
by using the rememberNavController()
method in the composable:
val navController = rememberNavController()
Execute provideNavController
, this function provides an instance of navController
to BiometricCompose
activity.
provider.provideController(navController)
Gets the instance of MBFaceStatus
, MBCaptureSessionStatus
and frameCollection
is the collection of frames. We observe in ComposeBiometric
activity.
val faceStatus: MBFaceStatus by captureSessionViewModel.faceStatus.observeAsState(MBFaceStatus.NOT_FOUND)
val frameCollection: List<Bitmap?> by frameCollectionViewModel.frameCollection.observeAsState(listOf())
Implement the NavHost
function. This Provides in place in the Compose hierarchy for self contained navigation to occur. The CaptureScreen
function gets as parameters captureService
that is the entry point configuration for the capture process, faceStatus
is the status of the face every time it changes and captureStatus
the status of the capture process every time it changes The ImageScreen
function displays the collection of frames and gets the frame frameCollection
as a parameter.
NavHost(navController = navController, startDestination = ScreensRoute.CaptureScreen.route) {
composable(ScreensRoute.CaptureScreen.route) {
CaptureScreen(captureSessionService = captureService, faceStatus = faceStatus, captureStatus = captureStatus)
}
composable(ScreensRoute.ImagesScreen.route) {
ImagesScreen(frames = frameCollection)
}
}
Implement FrameCollectionViewModel
, CaptureSessionViewModel
viewModel and NavControllerCallback
in ComposeBiometric
activity, as well as execute CaptureNavigation
and get the NavController
instance.
class ComposeBiometric : ComponentActivity() , MBCaptureSessionServiceDelegate,NavControllerCallback {
private lateinit var navController: NavController
private val framesViewModel: FrameCollectionViewModel by viewModels()
private val captureSessionViewMode: CaptureSessionViewModel by viewModels()
@SuppressLint("UnsafeOptInUsageError")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val options = MBCaptureSessionOptions.Builder()
.automaticCapture(true)
.build()
val captureSessionService = MBCaptureSessionService(
context = this, this as LifecycleOwner,
options, callback = this
)
setContent {
CaptureNavigation(
captureService = captureSessionService,
provider = this, captureSessionViewModel = captureSessionViewMode,
frameCollectionViewModel = framesViewModel
)
}
}
override fun onCaptureFinished(result: MBCaptureSessionResult?) {
this.runOnUiThread {
result?.let {
framesViewModel.setFrameCollection(it.frames)
navController.navigate(route = ScreensRoute.ImagesScreen.route)
}
}
}
override fun onCountdown(timeCounter: Int) { }
override fun onValidating(faceStatus: MBFaceStatus) {
this.runOnUiThread {
captureSessionViewMode.onFaceStatusChange(faceStatus)
}
}
override fun onFailure(errorEnum: MBCaptureSessionError) { }
override fun onCaptureStarted() { }
override fun provideController(navController: NavController) {
this.navController = navController
}
}
Creates instances of NavController
, FrameCollectionViewModel
and CaptureSessionViewModel
.
private lateinit var navController: NavController
private val framesViewModel: FrameCollectionViewModel by viewModels()
private val captureSessionViewMode: CaptureSessionViewModel by viewModels()
Starts CaptureNavigation
with captureSessionService
, provider
and the viewModes as parameters.
setContent {
CaptureNavigation(
captureService = captureSessionService,
provider = this, captureSessionViewModel = captureSessionViewMode,
frameCollectionViewModel = framesViewModel
)
}
Initialize the NavController
instance.
override fun provideController(navController: NavController) {
this.navController = navController
}
Execution of onCaptureFinished
.
override fun onCaptureFinished(result: MBCaptureSessionResult?) {
this.runOnUiThread {
result?.let {
framesViewModel.setFrameCollection(it.frames)
navController.navigate(route = ScreensRoute.ImagesScreen.route)
}
}
}
Provides the collection of frames to CaptureNavigation
composable.
framesViewModel.setFrameCollection(it.frames)
Navigates to ImageScreen
navController.navigate(route = ScreensRoute.ImagesScreen.route)
Execution of onValidating
.
override fun onValidating(faceStatus: MBFaceStatus) {
this.runOnUiThread {
captureSessionViewMode.onFaceStatusChange(faceStatus)
}
}
Updates the face status in the composable CaptureNavigation
.
captureSessionViewMode.onFaceStatusChange(faceStatus)